﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using Sebarf.Utils.Interfaces;

namespace Utils {
	/// <summary>
	/// At several places we need an object tree flattened. This class provides this functionality.
	/// It provides methods that receive an object and return all attached objects of the given object.
	/// Attached objects are found by field references. It protects against circular references. It uses
	/// a shared FieldInfoLookup table for type info to fasten performance. It also only respect 'real'
	/// data which means that event handler, delegates and so on are not processed.
	/// The functionality of this class is used, for example,
	/// - for getting all a list of all attached objects from a single object to have a list to loop over
	///   to collect original data so we can later compare the current data to the original data to build
	///   a change set,
	/// - on the collection mapper process chain step to have a list of objects to loop over to fill the
	///   mappers lookup table so that if a sub object is later queried we can return the already 
	///   retrieved object instead of a new object
	/// </summary>
	public class HieararchyFlattener {
		/// <summary>
		/// Our trace fields. We trace private and public instance fields over the whole inheritance hierarchy
		/// </summary>
		private static BindingFlags TraceFields = BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Public |
												  BindingFlags.NonPublic | BindingFlags.GetField;

		private List<ExtendMetadata> _ExtendedFlattenedList;
		private List<object> _FlattenedList;
		public Dictionary<Type, List<FieldInfo>> FieldInfoLookup { get; set; }

		public List<object> Flatten(object o) {
			if (FieldInfoLookup == null) {
				FieldInfoLookup = new Dictionary<Type, List<FieldInfo>>();
			}
			_FlattenedList = new List<object>();
			_ExtendedFlattenedList = new List<ExtendMetadata>();
			var temp = new Object();
			process(o, null, ref temp);
			return _FlattenedList;
		}

		/// <summary>
		/// returns all objects that are hold by fields
		/// </summary>
		/// <param name="o"></param>
		/// <returns></returns>
		public List<ExtendMetadata> ExtendedFlatten(object o) {
			if (FieldInfoLookup == null) {
				FieldInfoLookup = new Dictionary<Type, List<FieldInfo>>();
			}
			_FlattenedList = new List<object>();
			_ExtendedFlattenedList = new List<ExtendMetadata>();
			var temp = new Object();
			process(o, null, ref temp);
			return _ExtendedFlattenedList;
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="o">to be analyzed</param>
		/// <param name="collector">
		/// object[] args
		/// if (parent is IEnumerable)
		/// {
		///		Args[0]: Collection
		///		Args[1]: Entry
		/// }else
		/// {
		///		Args[0]: Parent
		///		Args[1]: Actual Element
		///		Args[2]: FieldInfo
		/// }
		/// , ref object output></param>
		/// <param name="output"></param>
		/// <returns></returns>
		public Object[] Flatten(object o, delMetadataCollector collector, ref object output) {
			FieldInfoLookup = new Dictionary<Type, List<FieldInfo>>();
			_FlattenedList = new List<object>();
			_ExtendedFlattenedList = new List<ExtendMetadata>();
			process(o, collector, ref output);
			object[] toReturn = _FlattenedList.ToArray();
			_FlattenedList.Clear();
			return toReturn;
		}

		private void process(object o, delMetadataCollector collector, ref object output) {
			if (_FlattenedList.Contains(o)) {
				return;
			}
			Type realtype = o.GetType();
			FillFieldInfoLookup(realtype);
			if (o is IEnumerable) {
				foreach (object listentry in (o as IEnumerable)) {
					if (listentry != null) {
						if (collector != null) {
							collector(new[] { o, listentry }, ref output);
						}
						process(listentry, collector, ref output);
					}
				}
			}
			else {
				_FlattenedList.Add(o);
				_ExtendedFlattenedList.Add(new ExtendMetadata { CurrentObject = o, ParentObject = null, ParentField = null });

				if (collector != null) {
					collector(new[] { null, o, null }, ref output);
				}
				foreach (FieldInfo field in FieldInfoLookup[realtype]) {
					object val = field.GetValue(o);
					if (val == null) {
						continue;
					}
					if (val is EventHandler || val is Delegate) {
						//TODO: don't process them (for submiting changes) as they are not real data. Or used anywhere?
					}
					else {
						_ExtendedFlattenedList.Add(new ExtendMetadata { CurrentObject = val, ParentObject = o, ParentField = field });
						if (collector != null) {
							collector(new[] { o, val, field }, ref output);
						}
						if (val != null && !val.GetType().IsPrimitive && !val.GetType().IsValueType && val.GetType() != typeof(String)) {
							process(val, collector, ref output);
						}
					}
				}
			}
		}

		/// <summary>
		/// Fills the field lookup table with fieldinfo's (that match our trace fields) from the given type
		/// </summary>
		/// <param name="t"></param>
		private void FillFieldInfoLookup(Type t) {
			if (!FieldInfoLookup.ContainsKey(t)) {
				FieldInfoLookup[t] = new List<FieldInfo>();
				Type workingType = t;
				while (workingType != null) {
					foreach (FieldInfo field in workingType.GetFields(TraceFields)) {
						if (typeof(EventHandler).IsAssignableFrom(field.FieldType) ||
							typeof(Delegate).IsAssignableFrom(field.FieldType)) {
							continue;
						}
						FieldInfoLookup[t].Add(field);
					}
					workingType = null;
					//	workingType = workingType.BaseType;
				}
			}
		}

		/// <summary>
		/// Returns all properties of a type which matches our trace fields
		/// </summary>
		/// <param name="t"></param>
		/// <returns></returns>
		public static PropertyInfo[] GetAllProperties(Type t) {
			var toReturn = new List<PropertyInfo>();
			Type workingType = t;
			while (workingType != null) {
				foreach (PropertyInfo field in workingType.GetProperties(TraceFields)) {
					toReturn.Add(field);
				}
				workingType = workingType.BaseType;
			}
			return toReturn.ToArray();
		}
	}
}